﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Text;
using System.Drawing;
using Common.Utilities;

namespace PdfLib
{
    /// <summary>
    /// search pdf page for pattern, 
    /// use either regex or string match
    /// score is 1.0 if perfect match
    /// otherwise score is LongestCommonSubString / pattern length
    /// </summary>
    public class PdfPatternSearcher
    {
        #region settings
        private static double _MinRelativeLineSpacing = 0.5;
        /// <summary>relative min line spacing over font size</summary>
        public static double MinRelativeLineSpacing
        {
            get
            {
                return _MinRelativeLineSpacing;
            }
            set
            {
                _MinRelativeLineSpacing = value;
            }
        }

        private static int _SlidingWindowHight = 5;
        /// <summary>line count</summary>
        public static int SlidingWindowHight
        {
            get
            {
                return _SlidingWindowHight;
            }
            set
            {
                _SlidingWindowHight = value;
            }
        }

        private static int _SlidingWindowStep = 1;
        /// <summary>slide 1 line each time</summary>
        public static int SlidingWindowStep
        {
            get
            {
                return _SlidingWindowStep;
            }
            set
            {
                _SlidingWindowStep = value;
            }
        }

        private static int _ShrinkingStep=12;
        /// <summary></summary>
        public static int ShrinkingStep
        {
            get
            {
                return _ShrinkingStep;
            }
            set
            {
                _ShrinkingStep = value;
            }
        }

        /// <summary>
        /// private static ctor, read settings from config file
        /// </summary>
        static PdfPatternSearcher()
        {
            if (!string.IsNullOrEmpty(ConfigurationManager.AppSettings.Get("MinRelativeLineSpacing")))
            {
                _MinRelativeLineSpacing = double.Parse(ConfigurationManager.AppSettings.Get("MinRelativeLineSpacing"));
            }
        }
        #endregion

        /// <summary>
        /// return rectangle of the last text token of the 
        /// first matched pattern
        /// </summary>
        /// <param name="pdfFilePath"></param>
        /// <param name="pageNum"></param>
        /// <param name="pattern">either a string or regex pattern</param>
        /// <param name="useRegex"></param>
        /// <param name="threshold">in the range (0,1)</param>
        /// <param name="matchScore">in the range [0,1]</param>
        /// <returns></returns>
        public static RectangleF FoundPattern(
            string pdfFilePath, int pageNum,
            string pattern, bool useRegex,
            double threshold, ref double matchScore)
        {
            SizeF pageSize = PdfPropertyReader.GetPageSize(pdfFilePath, pageNum);

            List<PdfTextToken> txtTokens = PdfTextReader.ReadPageContentAsTokens(pdfFilePath, pageNum);
            
            List<PdfTextLine> lineTokens = PdfTextReader.CreateLines(txtTokens);
            lineTokens = PdfTextReader.MergeLines(lineTokens, _MinRelativeLineSpacing);
            StringBuilder buffer = new StringBuilder();

            if(lineTokens.Count>SlidingWindowHight)
            {
                int fromLineIndex = 0;
                while (fromLineIndex < lineTokens.Count - SlidingWindowHight)
                {
                    buffer = new StringBuilder();
                    for (int k = fromLineIndex; k < fromLineIndex + SlidingWindowHight; k++)
                    {
                        buffer.Append(lineTokens[k].LineContent);
                        if (k < fromLineIndex + SlidingWindowHight - 1)
                        {
                            buffer.Append(Environment.NewLine);
                        }
                    }
                    double score = GetStringMatchScore(buffer.ToString(), pattern, useRegex);
                    if (score >= threshold)
                    {
                        int toLineIndex = fromLineIndex + SlidingWindowHight;
                        RectangleF rectF =
                            GetBoundingRectangle(
                                lineTokens, pattern, score, pageSize,
                                ref fromLineIndex, ref toLineIndex, useRegex);
                        fromLineIndex = toLineIndex + 1;
                        matchScore = score;
                        return rectF;
                    }
                    else
                    {
                        fromLineIndex += SlidingWindowStep;
                    }
                }
            }
            else
            {
                buffer = new StringBuilder();
                for(int i=0;i<lineTokens.Count;i++)
                {
                    buffer.Append(lineTokens[i].LineContent);
                    if (i < lineTokens.Count - 1)
                    {
                        buffer.Append(Environment.NewLine);
                    }
                }
                double score = GetStringMatchScore(buffer.ToString(), pattern, useRegex);
                if (score >= threshold)
                {
                    int fromLineIndex = 0;
                    int toLineIndex = lineTokens.Count-1;
                    RectangleF rectF = 
                        GetBoundingRectangle(
                            lineTokens, pattern, score, pageSize, 
                            ref fromLineIndex, ref toLineIndex, useRegex);
                    matchScore = score;
                    return rectF;
                }
            }
            return RectangleF.Empty;
        }


        /// <summary>
        /// return all matched position with their match scores
        /// </summary>
        /// <param name="pdfFilePath"></param>
        /// <param name="pageNum"></param>
        /// <param name="pattern"></param>
        /// <param name="useRegex"></param>
        /// <param name="threshold"></param>
        /// <returns></returns>
        public static Dictionary<RectangleF, double> FoundPatterns(
            string pdfFilePath, int pageNum,
            string pattern, bool useRegex,
            double threshold)
        {
            Dictionary<RectangleF, double> posScores = new Dictionary<RectangleF, double>();
            List<PdfTextToken> txtTokens = PdfTextReader.ReadPageContentAsTokens(pdfFilePath, pageNum);
            List<PdfTextLine> lineTokens = PdfTextReader.CreateLines(txtTokens);
            lineTokens = PdfTextReader.MergeLines(lineTokens, _MinRelativeLineSpacing);
            SizeF pageSize = PdfPropertyReader.GetPageSize(pdfFilePath, pageNum);

            StringBuilder buffer = new StringBuilder();
            if (lineTokens.Count > SlidingWindowHight)
            {
                int fromLineIndex = 0;
                while (fromLineIndex < lineTokens.Count - SlidingWindowHight)
                {
                    buffer = new StringBuilder();
                    for (int k = fromLineIndex; k < fromLineIndex + SlidingWindowHight; k++)
                    {
                        buffer.Append(lineTokens[k].LineContent);
                        if (k < fromLineIndex + SlidingWindowHight - 1)
                        {
                            buffer.Append(Environment.NewLine);
                        }
                    }
                    double score = GetStringMatchScore(buffer.ToString(), pattern, useRegex);
                    if (score >= threshold)
                    {
                        int toLineIndex = fromLineIndex + SlidingWindowHight;
                        RectangleF rectF =
                            GetBoundingRectangle(
                                lineTokens, pattern, score, pageSize,
                                ref fromLineIndex, ref toLineIndex, useRegex);
                        if(posScores.ContainsKey(rectF))
                        {
                            if(posScores[rectF]<score)
                            {
                                posScores.Add(rectF, score);
                                fromLineIndex = toLineIndex + 1;
                            }
                            else
                            {
                                fromLineIndex += SlidingWindowStep;
                            }
                        }
                        else
                        {
                            posScores.Add(rectF, score);
                            fromLineIndex = toLineIndex + 1;
                        }
                    }
                    else
                    {
                        fromLineIndex += SlidingWindowStep;
                    }
                }
            }
            else
            {
                buffer = new StringBuilder();
                for (int i = 0; i < lineTokens.Count; i++)
                {
                    buffer.Append(lineTokens[i].LineContent);
                    if (i < lineTokens.Count - 1)
                    {
                        buffer.Append(Environment.NewLine);
                    }
                }
                double score = GetStringMatchScore(buffer.ToString(), pattern, useRegex);
                if (score >= threshold)
                {
                    int fromLineIndex = 0;
                    int toLineIndex = lineTokens.Count - 1;
                    RectangleF rectF = 
                        GetBoundingRectangle(
                            lineTokens, pattern, score, pageSize,
                            ref fromLineIndex, ref toLineIndex, useRegex);
                    posScores.Add(rectF,score);
                }
            }
            return posScores;
        }

        #region match
        /// <summary>
        /// sliding window vertically to get maximal score that matches the pattern
        /// </summary>
        /// <param name="subjStr"></param>
        /// <param name="pattern"></param>
        /// <param name="useRegex"></param>
        /// <returns></returns>
        private static double GetStringMatchScore(string subjStr, string pattern, bool useRegex)
        {
            return StringUtil.FuzzyStringMatch(subjStr, pattern, useRegex);
            //StringMatchType matchType = StringMatchType.ExactMatch;
            //if (useRegex)
            //    matchType = StringMatchType.RegexMatch;
            //if (StringUtil.Match(subjStr, pattern, matchType))
            //    return 1.0;
            //else
            //{
            //    int commonSubSeqLen = 0;
            //    string[] patternWords = StringUtil.Split(pattern, " ");
            //    foreach(string patternWord in patternWords)
            //    {
            //        int commonSubStringLen = StringUtil.LongestCommonSubString(subjStr, patternWord);
            //        commonSubSeqLen += commonSubStringLen;
            //    }
            //    double score = (double)commonSubSeqLen / pattern.Length;
            //    return score;
            //}
        }

        /// <summary>
        /// shrink rectangle from all 4 sides inward while keep the match score
        /// </summary>
        /// <param name="lineTokens"></param>
        /// <param name="pattern"></param>
        /// <param name="matchScore"></param>
        /// <param name="pageSize"></param>
        /// <param name="fromLineIndex"></param>
        /// <param name="toLineIndex"></param>
        /// <param name="useRegex"></param>
        /// <returns></returns>
        private static RectangleF GetBoundingRectangle(
            List<PdfTextLine> lineTokens, string pattern, double matchScore,
            SizeF pageSize, ref int fromLineIndex, ref int toLineIndex, bool useRegex)
        {
            float top = lineTokens[fromLineIndex].Rect.Top;
            float bottom = lineTokens[toLineIndex].Rect.Bottom;
            float left = 0f;
            float right = pageSize.Width;
            int fromLineIdx2 = fromLineIndex;
            int toLineIdx2 = toLineIndex;

            // step 1: shrink vertically from top
            for(int i=fromLineIndex;i<=toLineIndex;i++)
            {
                StringBuilder buffer = new StringBuilder();
                for(int k=i;k<=toLineIndex;k++)
                {
                    buffer.Append(lineTokens[k].LineContent);
                    if (k < toLineIndex)
                        buffer.Append(Environment.NewLine);
                }
                double adjustedScore = GetStringMatchScore(buffer.ToString(), pattern, useRegex);
                if (adjustedScore < matchScore)
                {
                    if(i>fromLineIndex)
                    {
                        top = lineTokens[i - 1].Rect.Top;
                        fromLineIdx2 = i-1;
                    }
                    break;
                }
                else
                {
                    top = lineTokens[i].Rect.Top;
                    fromLineIdx2 = i;
                }
            }
            // step 2: shrink vertically from bottom
            for (int i = toLineIndex; i >= fromLineIdx2; i--)
            {
                StringBuilder buffer = new StringBuilder();
                for (int k = fromLineIdx2; k <= i; k++)
                {
                    buffer.Append(lineTokens[k].LineContent);
                    if (k < i)
                        buffer.Append(Environment.NewLine);
                }
                double adjustedScore = GetStringMatchScore(buffer.ToString(), pattern, useRegex);
                if (adjustedScore < matchScore)
                {
                    if(i<toLineIndex)
                    {
                        bottom = lineTokens[i + 1].Rect.Bottom;
                        toLineIdx2 = i + 1;
                    }
                    break;
                }
                else
                {
                    bottom = lineTokens[i].Rect.Bottom;
                    toLineIdx2 = i;
                }
            }

            // step 3: shrink horizontally from right
            while (right > left)
            {
                StringBuilder buffer = new StringBuilder();
                for (int k = fromLineIdx2; k <= toLineIdx2; k++)
                {
                    List<PdfTextToken> textTokens = lineTokens[k].TextTokens;
                    textTokens.Sort();
                    for (int m = 0; m < textTokens.Count; m++)
                    {
                        if (textTokens[m].TextRect.Left <= right)
                        {
                            buffer.Append(textTokens[m].Text);
                            if (m < textTokens.Count - 1)
                            {
                                buffer.Append(" ");
                            }
                        }
                    }

                    if (k < toLineIdx2 - 1)
                        buffer.Append(Environment.NewLine);
                }
                double adjustedScore = GetStringMatchScore(buffer.ToString(), pattern, useRegex);
                if (adjustedScore < matchScore)
                {
                    right += _ShrinkingStep;
                    break;
                }
                else
                {
                    if (right - _ShrinkingStep > left)
                        right -= _ShrinkingStep;
                    else
                        break;
                }
            }

            // step 4: shrink horizontally from left 
            while (left < right)
            {
                StringBuilder buffer = new StringBuilder();
                for (int k = fromLineIdx2; k <= toLineIdx2; k++)
                {
                    List<PdfTextToken> textTokens = lineTokens[k].TextTokens;
                    textTokens.Sort();
                    for (int m = 0; m < textTokens.Count; m++)
                    {
                        if (textTokens[m].TextRect.Right >= left)
                        {
                            buffer.Append(textTokens[m].Text);
                            if (m < textTokens.Count - 1)
                            {
                                buffer.Append(" ");
                            }
                        }
                    }

                    if (k < toLineIdx2 - 1)
                        buffer.Append(Environment.NewLine);
                }
                double adjustedScore = GetStringMatchScore(buffer.ToString(), pattern, useRegex);
                if (adjustedScore < matchScore)
                {
                    left -= _ShrinkingStep;
                    break;
                }
                else
                {
                    if (left + _ShrinkingStep < right)
                        left += _ShrinkingStep;
                    else
                        break;
                }
            }

            // step 5: adjust left/right to allow intersecting text tokens to be included
            for(int i=fromLineIdx2;i<=toLineIdx2;i++)
            {
                List<PdfTextToken> textTokens = lineTokens[i].TextTokens;
                textTokens.Sort();
                for(int j=0;j<textTokens.Count;j++)
                {
                    if(textTokens[j].TextRect.Left < left && textTokens[j].TextRect.Right > left)
                    {
                        left = textTokens[j].TextRect.Left;
                    }
                    if(textTokens[j].TextRect.Right > right && textTokens[j].TextRect.Left < right)
                    {
                        right = textTokens[j].TextRect.Right;
                    }
                }
            }
            
            // step 6. fine tuning left/right position to just enough to include 
            // all included text tokens
            List<PdfTextToken> includedTextTokens = new List<PdfTextToken>();
            for(int i=fromLineIdx2; i<=toLineIdx2;i++)
            {
                List<PdfTextToken> textTokens = lineTokens[i].TextTokens;
                textTokens.Sort();
                for(int k=0;k<textTokens.Count;k++)
                {
                    if(textTokens[k].TextRect.Right >=left && textTokens[k].TextRect.Left<=right)
                    {
                        includedTextTokens.Add(textTokens[k]);
                    }
                }
            }
            if (includedTextTokens.Count > 0)
            {
                includedTextTokens.Sort();
                left = includedTextTokens[0].TextRect.Left;
                right = includedTextTokens[includedTextTokens.Count - 1].TextRect.Right;
                foreach (PdfTextToken textToken in includedTextTokens)
                {
                    if (textToken.TextRect.Left < left)
                        left = textToken.TextRect.Left;
                    if (textToken.TextRect.Right > right)
                        right = textToken.TextRect.Right;
                }
            }

            fromLineIndex = fromLineIdx2;
            toLineIndex = toLineIdx2;

            return new RectangleF(left, top, right - left, bottom - top);
        }
        #endregion
    }
}
